Skip to content

[Bug][API] Make kubectl get modeladapter show a truthful status - #2624

Merged
Jeffwan merged 6 commits into
vllm-project:mainfrom
yaojiejia:yaojiejia/feat-modeladapter-status-columns
Sep 8, 2026
Merged

[Bug][API] Make kubectl get modeladapter show a truthful status#2624
Jeffwan merged 6 commits into
vllm-project:mainfrom
yaojiejia:yaojiejia/feat-modeladapter-status-columns

Conversation

@yaojiejia

Copy link
Copy Markdown
Contributor

Pull Request Description

kubectl get modeladapter already had Phase/Desired/Ready/Candidates columns, but the controller often left them blank, stale or wrong, and nothing said why an adapter was not ready.

Problems fixed

  1. In single-pod mode, while waiting for pods, Candidates and DesiredReplicas were computed but never saved. The adapter showed Pending with empty counters and no reason.
  2. When a Running adapter lost all its pods, ReadyReplicas was not recomputed and the EndpointSlice code re-set Phase=Running, so kubectl get showed Running Ready=1 Candidates=0.
  3. There was no column showing why an adapter is not ready.

Changes

  • Controller: one helper (recomputeReadiness) now derives ReadyReplicas, Phase and the Ready condition from the loaded instances at the end of every reconcile. Running is only set when at least one instance is loaded. The "waiting for pods" path now saves the counters with a PodNotReady or ModelAdapterUnavailable reason. Failed is kept until a load succeeds. Status is written only when something changed.
  • CRD: new Reason column (from the Ready condition). Base Model, Instances and Message are -o wide columns.
  • Tests: unit tests for the helper plus fake-client DoReconcile tests for the three scenarios above (they fail on the old controller).
  • Docs: LoRA page lifecycle now matches the code (no Loading phase, Bound is not the success state) and shows sample kubectl get output.

$ kubectl get modeladapter
NAME PHASE DESIRED READY CANDIDATES REASON MODEL PATH AGE
qwen-code-lora Pending PodNotReady hf://... 5s
qwen-code-lora Running 1 1 1 ModelAdapterAvailable hf://... 2m

Behavior changes to note

  • In single-pod mode, if the pod goes away and no replacement is ready, status flips to Pending/PodNotReady right away instead of keeping the stale Running. The AdapterMigrating/Rescheduled conditions still appear when a replacement pod exists.
  • Failed stays until a load succeeds, even if all pods are gone.
  • Instances renders as a JSON list in -o wide.

Testing

make manifests-all, make verify-crd, hack/verify-codegen.sh, make lint-all, go test ./pkg/controller/modeladapter/... ./api/... (with and without -race) all pass.

Related Issues

Resolves: #238

Important: Before submitting, please complete the description above and review the checklist below.


Contribution Guidelines (Expand for Details)

We appreciate your contribution to aibrix! To ensure a smooth review process and maintain high code quality, please adhere to the following guidelines:

Pull Request Title Format

Your PR title should start with one of these prefixes to indicate the nature of the change:

  • [Bug]: Corrections to existing functionality
  • [CI]: Changes to build process or CI pipeline
  • [Docs]: Updates or additions to documentation
  • [API]: Modifications to aibrix's API or interface
  • [CLI]: Changes or additions to the Command Line Interface
  • [Misc]: For changes not covered above (use sparingly)

Note: For changes spanning multiple categories, use multiple prefixes in order of importance.

Submission Checklist

  • PR title includes appropriate prefix(es)
  • Changes are clearly explained in the PR description
  • New and existing tests pass successfully
  • Code adheres to project style and best practices
  • Documentation updated to reflect changes (if applicable)
  • Thorough testing completed, no regressions introduced

By submitting this PR, you confirm that you've read these guidelines and your changes align with the project's contribution standards.

Signed-off-by: Alex Jia <yj2761@nyu.edu>
Signed-off-by: Alex Jia <yj2761@nyu.edu>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the lifecycle phase and readiness status logic for ModelAdapter to ensure that the status accurately reflects the state of loaded instances, preventing premature 'Running' or 'Ready=True' states. It introduces a centralized readiness recomputation mechanism, updates CRD print columns, and updates the documentation. A comprehensive suite of unit tests is also added. The review feedback suggests refactoring the updateStatus logging logic to prevent misleading log messages when status changes are persisted.

Comment on lines +1070 to +1076
func (r *ModelAdapterReconciler) syncReadinessStatus(ctx context.Context, oldInstance, instance *modelv1alpha1.ModelAdapter) error {
recomputeReadiness(instance)
if !r.inconsistentModelAdapterStatus(oldInstance.Status, instance.Status) {
return nil
}
return r.updateStatus(ctx, instance)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function correctly centralizes the status update logic. However, the call to r.updateStatus(ctx, instance) on line 1075 can lead to confusing logs. The updateStatus function logs a changed flag which will be false in this call path, even though the status is changing (as determined by inconsistentModelAdapterStatus).

To improve log clarity, consider a small refactor of the updateStatus function to make its logging more accurate. For example:

func (r *ModelAdapterReconciler) updateStatus(ctx context.Context, instance *modelv1alpha1.ModelAdapter, conditions ...metav1.Condition) error {
	var conditionsChanged bool
	for _, condition := range conditions {
		if meta.SetStatusCondition(&instance.Status.Conditions, condition) {
			conditionsChanged = true
		}
	}
	klog.InfoS("model adapter reconcile", "Updating CR status", "instance", instance.Name, "conditionsChanged", conditionsChanged, "status", instance.Status)
	return r.Status().Update(ctx, instance)
}

This change clarifies that the logged flag refers specifically to whether the passed-in conditions caused a change, making debugging easier.

@varungup90

Copy link
Copy Markdown
Collaborator

🧵 Code Review Comments

🚨 Severe (Should fix before merge)

1. Semantic clash between wait path + recomputeReadiness and merged [#2625](#2624)

  • Location: pkg/controller/modeladapter/modeladapter_controller.go
  • Rule: PP-7 (see the whole picture) + CC-5 (don’t mislead) + CC-14 (one word per concept)
  • Why: This PR is based on 7540088 (Aug 26). Merged [#2625]([Bug][API] Make kubectl get modeladapter show a truthful status #2624) already persists Scheduled=False with NoReadyPods / InsufficientReadyPods on the same wait path. GitHub will merge this cleanly because the hunks don’t overlap, but the merged controller would:
  1. Write status once from reconcileLoadOnSinglePod (Scheduled reason, stale Phase / ReadyReplicas).
  2. Write again from syncReadinessStatus (Ready reason from a coarser model).

On a ready-but-unstable pod, Candidates > 0, so the new Reason column becomes ModelAdapterUnavailable (“not loaded on N candidate pods”) while Scheduled still says NoReadyPods. Those are two different stories for the same wait.


2. recomputeReadiness Failed short-circuit — Reason column goes stale when pods disappear

  • Location: pkg/controller/modeladapter/modeladapter_controller.go (~lines 1055–1056)
  • Rule: CC-153 (boundary behavior) + PP-75 (status must match what the user asked to see)
  • Why: If Phase == Failed and ReadyReplicas == 0, the helper leaves the old ModelAdapterLoadingError in place even when Candidates == 0. The new Reason column then still displays "loading error" after a scale-to-zero or total pod loss. While "failed-until-success" is a fair product choice for the phase, it creates an inaccurate glanceable reason.
  • Fix: Keep Phase=Failed if you want the load error to remain sticky, but refresh the Ready reason/message when the current blocker is "no pods" (or combine both facts in the message). Call the helper on the loading/service/EPS error returns too, so Bound/ResourceCreated cannot leave a leftover Ready reason in the column.
  • Cost: Low — an extra conditional branch plus one table-test case (Failed + Candidates==0 already exists for phase; assert the Ready reason).
  • Benefit: High — Reason is the exact column this PR introduces; a stale reason is the primary bug class this PR aims to eliminate.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new recomputeReadiness currently makes the Scheduled phase effectively unobservable (it gets overwritten to Pending), conflicting with the documented lifecycle and single-pod scheduling behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves the correctness and observability of kubectl get modeladapter by making the controller consistently recompute and persist readiness-related status fields, and by surfacing the Ready condition reason/message via CRD printer columns and docs.

Changes:

  • Controller: adds recomputeReadiness/syncReadinessStatus to derive ReadyReplicas, Phase, and the Ready condition from the final instance set and to avoid stale “Running/Ready” states.
  • API/CRD: adds Reason (and Base Model/Instances/Message as wide columns) to make non-ready causes visible in kubectl get.
  • Tests/docs/samples: adds controller tests for the new readiness behavior and updates lifecycle documentation and sample manifests.
File summaries
File Description
samples/adapter/adapter.yaml Updates lifecycle phase documentation comment to match the new status model.
pkg/controller/modeladapter/modeladapter_status_test.go Adds unit + fake-client tests covering readiness recomputation and reconcile scenarios.
pkg/controller/modeladapter/modeladapter_controller.go Introduces readiness recomputation and sync logic; removes EndpointSlice-driven “force Running”.
docs/source/features/lora-dynamic-loading.rst Updates lifecycle docs and adds example kubectl get outputs including reason/message.
dist/chart/crds/model.aibrix.ai_modeladapters.yaml Adds printer columns (Reason/Base Model/Instances/Message) to the shipped CRD.
config/crd/model/model.aibrix.ai_modeladapters.yaml Adds the same printer columns to the source CRD manifest.
api/model/v1alpha1/modeladapter_types.go Adds kubebuilder printcolumn annotations for the new columns.
Review details
  • Files reviewed: 6/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1070 to +1085
switch {
case status.ReadyReplicas > 0:
status.Phase = modelv1alpha1.ModelAdapterRunning
meta.SetStatusCondition(&status.Conditions, NewCondition(string(modelv1alpha1.ModelAdapterConditionReady), metav1.ConditionTrue,
ModelAdapterAvailable, fmt.Sprintf("ModelAdapter %s is ready", klog.KObj(instance))))
case status.Phase == modelv1alpha1.ModelAdapterFailed:
// Keep the loading error recorded by reconcileLoading visible.
case status.Candidates == 0:
status.Phase = modelv1alpha1.ModelAdapterPending
meta.SetStatusCondition(&status.Conditions, NewCondition(string(modelv1alpha1.ModelAdapterConditionReady), metav1.ConditionFalse,
PodNotReadyReason, "no ready pods match the pod selector"))
default:
status.Phase = modelv1alpha1.ModelAdapterPending
meta.SetStatusCondition(&status.Conditions, NewCondition(string(modelv1alpha1.ModelAdapterConditionReady), metav1.ConditionFalse,
ModelAdapterUnavailable, fmt.Sprintf("adapter is not loaded on any of the %d candidate pods", status.Candidates)))
}
@Jeffwan

Jeffwan commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

@yaojiejia could you rebase the main branch?

…dapter-status-columns

Signed-off-by: Alex Jia <yj2761@nyu.edu>
@yaojiejia

Copy link
Copy Markdown
Contributor Author

@yaojiejia could you rebase the main branch?

just rebased

@Jeffwan
Jeffwan merged commit b7d4573 into vllm-project:main Sep 8, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should ModelAdapter displays the Status on the result of get command

4 participants